{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 3\n",
    "\n",
    "### <center> Автор материала: Кощегулов Эльдар, allincool@mail.ru"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных </center>\n",
    "## <center> Классификация спама в SMS </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Цель работы. \n",
    "Задача состоит в том, чтобы построить модель классификации спам сообщений в SMS, на основе имеющихся данных.\n",
    "#### Входные данные.\n",
    "Решаться задача будет на датасете взятом тут: https://www.kaggle.com/uciml/sms-spam-collection-dataset\n",
    "\n",
    "* v1 метка spam/ham\n",
    "* v2 текст sms\n",
    "\n",
    "Целевой признак является метка spam/ham является ли SMS спамом или нет"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Первичный анализ "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import seaborn as sns\n",
    "import scipy\n",
    "import matplotlib.pyplot as plt\n",
    "from nltk.stem.wordnet import WordNetLemmatizer\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "from sklearn.model_selection import train_test_split, validation_curve\n",
    "from sklearn.metrics import roc_auc_score, precision_score\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.linear_model import LogisticRegression, LogisticRegressionCV\n",
    "from sklearn.naive_bayes import MultinomialNB\n",
    "from sklearn.model_selection import StratifiedKFold\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(module='sklearn*', action='ignore', category=DeprecationWarning)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('../../data/spam.csv', encoding='latin-1')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### В заявленных признаках v1 и v2 пропущенных значений нет. Видим что помимо признаков v1 и v2 имеем еще 3 признака. Скорее всего это какой-то мусор"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['Unnamed: 2'].unique()[: 5]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[df['Unnamed: 2'] ==  ' PO Box 5249']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Добавим данные из трех \"левых\" столбцов к тесту SMS и удалим их. Переименуем признаки. Для удобства переобозначим метки\n",
    "* spam - 1\n",
    "* ham - 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['v2'] = df['v2'] + df['Unnamed: 2'].fillna('') + df['Unnamed: 3'].fillna('') + df['Unnamed: 4'].fillna('')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop(columns = ['Unnamed: 2', 'Unnamed: 3', 'Unnamed: 4'], inplace = True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.rename(columns = {'v1' : 'label', 'v2' : 'sms'}, inplace = True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['label'] = df['label'].map({'spam' : 1, 'ham' : 0})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Датасет содержит 5572 объекта. Теперь пропущенных значений в нем нет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Посмотрим как выглядит обычное SMS и спам SMS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[df['label'] == 0].sample(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[df['label'] == 1].sample(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### В спам-сообщениях часто много заглавных букв, восклицательных знаков, и чисел, типа поздравляем вы выиграли миллион"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Посмотрим на распределение классов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "_, ax = plt.subplots()\n",
    "plt.bar(np.arange(2), df['label'].value_counts(), color = ['green', 'red'])\n",
    "ax.set_xticks(np.arange(2))\n",
    "ax.set_xticklabels(['ham', 'spam']);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['label'].value_counts()[1] / df.shape[0], df['label'].value_counts()[0] / df.shape[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Видим что классы несбалансированы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Инсайты"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Здравый смысл подсказывает, что обычно в спам сообщениях вам пишут какие-то левые люди, которые представляются вашими друзьями и зовут куда-то зарегистрироваться или вас поздравляют с выигрышами в лотерею. Значит признаки большого количества заглавных букв, обилия знаков препинания и чисел в текстах сообщений, должны что-то дать"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Генерация признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Пока что будем генерировать признаки для объединенной выборки. Удалим знаки препинания, удалим опечатки, приведем тексты к нижнему регистру, сгенерируем признаки длина текста, число знаков препинания, наличие символа, не являющегося цифрой или буквой алфавита."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Первым признаком который мы создадим будет длина SMS. Обычно SMS имеют ограничения на количество слов, поэтому спамеры чтобы не платить много денежек стараются не превосходить эту длину"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['len'] = df['sms'].apply(lambda x : len(x.strip().split()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Создадим счетчик знаков препинания в тексте SMS, а затем удалим знаки препинания. В идеале нужен счетчик восклицательных знаков, так как в спаме вас обычно поздравляют с выигрышами в лотереях и прочем и используют много восклицаний"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import regex as re    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['punctuation'] = df['sms'].apply(lambda x : len(re.findall(\"[^\\P{P}-]+\", x)))\n",
    "df['punctuation'] = df['sms'].apply(lambda x : len(re.findall(\"[^\\P{P}-]+\", x)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['sms'] = df['sms'].apply(lambda x : re.sub(\"[^\\P{P}-]+\", \"\", x))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Создадим счетчик заглавных букв в тексте SMS, а затем приведем тексты к нижнему регистру.. Зачастую в спам сообщениях пишут капсом."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['capital'] = df['sms'].apply(lambda x : sum(1 for c in x if c.isupper()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['sms'] = df['sms'].apply(lambda x : str.lower(x))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Посмотрим какие символы встречаются в текстах. Видим что помимо букв и цифр еще встречается много мусора. Создадим бинарный признак: содержит ли текст SMS символ кроме буквы и цифры. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "symbols = {}\n",
    "for x in [item for sublist in list(map(list, df['sms'].tolist())) for item in sublist] :\n",
    "    if x in symbols :\n",
    "        symbols[x] += 1\n",
    "    else :\n",
    "        symbols[x] = 1\n",
    "symbols"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "volwes = 'aeiou'\n",
    "consonant = 'bcdfghjklmnpqrstvwxyz'\n",
    "digits = '0123456789'\n",
    "alphabet = set(volwes) | set(consonant) | set(digits)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "len(alphabet)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bad_symbols = [x for x in symbols if x not in alphabet]\n",
    "bad_symbols = ''.join(set(bad_symbols) - set(' '))\n",
    "bad_symbols"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['badsymbol'] = df['sms'].apply(lambda x :1 if len([s for s in x if s in bad_symbols]) > 0 else 0)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Попробуем исправить опечатки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['sms'] = df['sms'].str.replace('å', 'a').str.replace('ä', 'a').str.replace('â', 'a').str.replace('á', 'a')\n",
    "df['sms'] = df['sms'].str.replace('õ', 'o').str.replace('ò', 'o').str.replace('ð', 'o').str.replace('ö', '0') \\\n",
    "                    .str.replace('ó', 'o').str.replace('ô', 'o')\n",
    "df['sms'] = df['sms'].str.replace('û', 'u')\n",
    "df['sms'] = df['sms'].str.replace('è', 'e')\n",
    "df['sms'] = df['sms'].str.replace('ì', '1').str.replace('ï', 'l')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### В спам сообщениях часто упоминаются крупные денежные выигрыши. Нужно создать признаки : наличие числа в тексте и наличие символа валюты"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Замечаем что среди символов в текстах имеются '$' и '£'. Создадим признак для них."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['moneysign'] = df['sms'].apply(lambda x : 1 if ('$' in list(x)) or ('£' in list(x)) else 0 )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "####  Остальные символы поудаляем. Вообще, возможно что при удалении знаков препинания мы поудаляли смайлы и возможно наличие/отсутствие смайла будет хорошим признаком. Если будет время надо подумать над этим. Признак исправлял ли я или нет"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "symbols = {}\n",
    "for x in [item for sublist in list(map(list, df['sms'].tolist())) for item in sublist] :\n",
    "    if x in symbols :\n",
    "        symbols[x] += 1\n",
    "    else :\n",
    "        symbols[x] = 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bad_symbols = [x for x in symbols if x not in alphabet]\n",
    "bad_symbols = ''.join(set(bad_symbols) - set(' '))\n",
    "bad_symbols"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for symb in bad_symbols : \n",
    "    df['sms'] = df['sms'].str.replace(symb, '')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "symbols = {}\n",
    "for x in [item for sublist in list(map(list, df['sms'].tolist())) for item in sublist] :\n",
    "    if x in symbols :\n",
    "        symbols[x] += 1\n",
    "    else :\n",
    "        symbols[x] = 1\n",
    "symbols"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Создадим признак: наличие в тексте SMS числа(возможно надо проверять не просто число, а число с множествой нулей)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['num'] = df['sms'].apply(lambda x : 1 if len([s for s in x if s in digits]) > 0 else 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.columns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Полезность признаков будем исследовать в дальнейшем с помощью модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Разобьем данные на трейн и тест с одинаковым распределением целевой переменной"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "target = df['label'].values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test = train_test_split(df, target, test_size = 0.2, stratify = target, random_state = 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_train.sum() / len(y_train), y_test.sum() / len(y_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train.shape, X_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### В трейне 4457 объектов, в тесте 1115"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Визуальный анализ"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Построим гистограммы созданных признаков слева и гистограммы созданных признаков в зависимости от целевой переменной справа"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in X_train.columns[2 :] :\n",
    "    fig, axes = plt.subplots(nrows = 1, ncols = 2, figsize = (20, 10))\n",
    "#     ax.set_ylabel('% фрагментов', fontsize=12)\n",
    "#     ax.set_xlabel('Имя автора', fontsize=12) \n",
    "    axes[0].set_title(col)\n",
    "    axes[0].hist(X_train[col], bins = 200);\n",
    "    axes[1].set_title(col)\n",
    "    axes[1].hist(X_train[col][X_train['label'] == 0], bins = 200, label = 'ham')\n",
    "    axes[1].hist(X_train[col][X_train['label'] == 1], bins = 200, label = 'spam')\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Судя по гистограммам признаов, почти все спам сообщения содержат символ валюты. Также половина спам сообщений содержит число в своем тексте и опечатку. При генерации этих признаков подобный эффект и ожидался."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize = (20, 10))\n",
    "sns.heatmap(X_train[['label', 'len', 'punctuation', 'capital', 'badsymbol',\n",
    "       'moneysign', 'num']].corr())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### - Во-первых, длина SMS коррелирует с числом гласных/согласных, числом знаков препинания, тут ничего удивительного.\n",
    "#### - Во-вторых, видим корреляцию между наличием символа, не являющегося цифрой или буквой алфавита, и наличием символов \"$\" и \"£\", так как второе является подмножество первого.\n",
    "#### - В-третьих, видим корреляцию между целевой переменной и наличием числа в тексте SMS и наличием символа денежки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Решается задача классификации на два класса. Классы несбалансированы, FP - нормальное SMS помечено как спам, это недопустимо. FN - спам помечен как нормальное SMS, допустимо, но не сильно хочется. Поэтому в качестве метрики будем использовать rocauc."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### На заре развития спам-фильтров их строили используя наивный байесовский классификатор, поэтому будем рассматривать эту модель. Также у нас ожидается много признаков после использования преобразования tfidf к тексту SMS, поэтому будем рассматривать логистическую регрессию. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Будем использовать преобразование tfidf для текста SMS, так же отмасштабируем признаки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cols = ['len', 'punctuation', 'capital', 'badsymbol', 'moneysign', 'num']\n",
    "X_train_scaled = pd.DataFrame(scaler.fit_transform(X_train[cols]), columns = cols)\n",
    "X_test_scaled = pd.DataFrame(scaler.transform(X_test[cols]), columns = cols)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Данных у нас не так много, поэтому выбираем кросс-валидацию на 10 фолдов. Для начала посмотрим на наши модели из коробки, ничего не настраивая."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def valid(model, n, bayes = False) :\n",
    "    skf = StratifiedKFold(n_splits = n, random_state = 17)\n",
    "    auc_scores = []\n",
    "    for train_index, valid_index in skf.split(X_train_scaled, y_train):\n",
    "        X_train_part, X_valid = X_train_scaled.iloc[train_index], X_train_scaled.iloc[valid_index]\n",
    "        y_train_part, y_valid = y_train[train_index], y_train[valid_index]\n",
    "        \n",
    "        X_train_sms, X_valid_sms = X_train.iloc[train_index]['sms'], X_train.iloc[valid_index]['sms']\n",
    "        cv = TfidfVectorizer(ngram_range = (1, 3))\n",
    "        X_train_bow = cv.fit_transform(X_train_sms)\n",
    "        X_valid_bow = cv.transform(X_valid_sms)     \n",
    "        if bayes :\n",
    "            X_train_new = X_train_bow\n",
    "            X_valid_new = X_valid_bow\n",
    "        else :\n",
    "            X_train_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_bow, X_train_part]))\n",
    "            X_valid_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_valid_bow, X_valid]))\n",
    "        model.fit(X_train_new, y_train_part)\n",
    "        model_pred_for_auc = model.predict_proba(X_valid_new)\n",
    "        auc_scores.append(roc_auc_score(y_valid, model_pred_for_auc[:, 1]))\n",
    "    return np.mean(auc_scores)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(random_state = 17)\n",
    "bayes = MultinomialNB()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores_logit = valid(logit, 10)\n",
    "print('Logistic regreession - rocauc : {}'.format(scores_logit))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores_bayes = valid(bayes, 10, True)\n",
    "print('Bayessian classfier - rocauc : {}'.format(scores_bayes))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Видим, что логистическая регрессия справляется получше. Дальше будем работать только с ней."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Настройка гиперпараметров и построение кривых валидации и обучения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def valid_for_valid_plots(model, n, bayes = False) :\n",
    "    skf = StratifiedKFold(n_splits = n, random_state = 17)\n",
    "    auc_scores_cv = []\n",
    "    auc_scores_valid = []\n",
    "    for train_index, valid_index in skf.split(X_train_scaled, y_train):\n",
    "        X_train_part, X_valid = X_train_scaled.iloc[train_index], X_train_scaled.iloc[valid_index]\n",
    "        y_train_part, y_valid = y_train[train_index], y_train[valid_index]\n",
    "        \n",
    "        X_train_sms, X_valid_sms = X_train.iloc[train_index]['sms'], X_train.iloc[valid_index]['sms']\n",
    "        cv = TfidfVectorizer(ngram_range = (1, 3))\n",
    "        X_train_bow = cv.fit_transform(X_train_sms)\n",
    "        X_valid_bow = cv.transform(X_valid_sms)     \n",
    "        if bayes :\n",
    "            X_train_new = X_train_bow\n",
    "            X_valid_new = X_valid_bow\n",
    "        else :\n",
    "            X_train_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_bow, X_train_part]))\n",
    "            X_valid_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_valid_bow, X_valid]))\n",
    "            \n",
    "        model.fit(X_train_new, y_train_part)\n",
    "        auc_scores_cv.append(roc_auc_score(y_train_part, model.predict_proba(X_train_new)[:, 1]))\n",
    "        model_pred_for_auc = model.predict_proba(X_valid_new)\n",
    "        auc_scores_valid.append(roc_auc_score(y_valid, model_pred_for_auc[:, 1]))\n",
    "    return 1 - np.mean(auc_scores_valid), 1 - np.mean(auc_scores_cv)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Построим кривые валидации"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Cs = [0.1 * i for i in range(1, 21)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scores = []\n",
    "for c in Cs :\n",
    "    logit = LogisticRegression(C = c, random_state = 17)\n",
    "    scores.append(valid_for_valid_plots(logit, 10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(nrows = 1, ncols = 1, figsize = (20, 10))\n",
    "plt.plot(Cs, [i[0] for i in scores], color = 'blue', label='holdout')\n",
    "plt.plot(Cs, [i[1] for i in scores], color = 'red', label='CV')\n",
    "plt.ylabel(\"ROCAUC\")\n",
    "plt.xlabel(\"C\")\n",
    "plt.title('Validation curve for C in (0.1, 2)');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "####  Будем перебирать значения C в интервале [0.5, 1.5]. При С < 0.5 происходит недообучение. При С > 1.5 ошибка на трейне упирается в ноль,  а на валидации не падает, это переобучение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Cs = np.linspace(0.5, 1.5, 10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for c in Cs :\n",
    "    logit = LogisticRegression(C = c, random_state = 17)\n",
    "    print(c, valid(logit, 10))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### C_opt = 1.5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C_opt = 1.5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Построим кривые обучения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def valid_for_train_plots(model, n, alpha, bayes = False) :\n",
    "    skf = StratifiedKFold(n_splits = n, random_state = 17)\n",
    "    auc_scores_cv = []\n",
    "    auc_scores_valid = []\n",
    "    for train_index, valid_index in skf.split(X_train_scaled[: int(X_train_scaled.shape[0] * alpha)], y_train[: int(X_train_scaled.shape[0] * alpha)]):\n",
    "        X_train_part, X_valid = X_train_scaled.iloc[train_index], X_train_scaled.iloc[valid_index]\n",
    "        y_train_part, y_valid = y_train[train_index], y_train[valid_index]\n",
    "        \n",
    "        X_train_sms, X_valid_sms = X_train.iloc[train_index]['sms'], X_train.iloc[valid_index]['sms']\n",
    "        cv = TfidfVectorizer(ngram_range = (1, 3))\n",
    "        X_train_bow = cv.fit_transform(X_train_sms)\n",
    "        X_valid_bow = cv.transform(X_valid_sms)     \n",
    "        if bayes :\n",
    "            X_train_new = X_train_bow\n",
    "            X_valid_new = X_valid_bow\n",
    "        else :\n",
    "            X_train_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_bow, X_train_part]))\n",
    "            X_valid_new = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_valid_bow, X_valid]))\n",
    "            \n",
    "        model.fit(X_train_new, y_train_part)\n",
    "        auc_scores_cv.append(roc_auc_score(y_train_part, model.predict_proba(X_train_new)[:, 1]))\n",
    "        model_pred_for_auc = model.predict_proba(X_valid_new)\n",
    "        auc_scores_valid.append(roc_auc_score(y_valid, model_pred_for_auc[:, 1]))\n",
    "    return np.mean(auc_scores_valid), np.mean(auc_scores_cv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "alphas = [0.1 * i for i in range(1, 11)]\n",
    "scores = []\n",
    "for alpha in  alphas :\n",
    "    logit = LogisticRegression(C = C_opt, random_state = 17)\n",
    "    scores.append(valid_for_train_plots(logit, 10, alpha = alpha))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(nrows = 1, ncols = 1, figsize = (20, 10))\n",
    "plt.plot(alphas, [i[0] for i in scores], color = 'blue', label='holdout')\n",
    "plt.plot(alphas, [i[1] for i in scores], color = 'red', label='CV')\n",
    "plt.ylabel(\"ROCAUC\")\n",
    "plt.xlabel(\"C\")\n",
    "plt.title('Learnings curve with optimal C');"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Судя по кривым обучения, происходит недообучение и для улучшения результата надо усложнить модель."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Прогноз для тестовой выборки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cv = TfidfVectorizer(ngram_range = (1, 3))\n",
    "X_train_sms = cv.fit_transform(X_train['sms'])\n",
    "X_test_sms = cv.transform(X_test['sms'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_train_sms, X_train_scaled]))\n",
    "test = scipy.sparse.csr_matrix(scipy.sparse.hstack([X_test_sms, X_test_scaled]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit = LogisticRegression(C = C_opt, random_state = 17)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit.fit(train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for x, y in zip(cols, logit.coef_[0][len(cv.get_feature_names()) :]) :\n",
    "    print(x, y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Видим, что для нашей модели признаки наличия числа и наличие символа валюты в тексте SMS являются важными, также число слов в тексте и число заглавных букв, а вот признаки наличия опечаток и знаков препинания не так уж и важны."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit_pred = logit.predict_proba(test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "roc_auc_score(y_test, logit_pred[:, 1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Качество на тесте соответствует ожиданиям после кросс-валидации"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Предложено решение задачи фильтрации спама на основе модели логистической регрессии. Можно использовать подобные спам-фильтры для SMS, электронной почты.\n",
    "\n",
    "#### Дальнейшее развитие модели может быть связано с лемматизацией/стеммингом текстов SMS. Использовать стекинг/блендинг нескольких моделей."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
